Skip to content

wire dash + the "open BAND" positioning (P1 + P3)#360

Merged
laulpogan merged 10 commits into
mainfrom
observability-open-band
Jul 6, 2026
Merged

wire dash + the "open BAND" positioning (P1 + P3)#360
laulpogan merged 10 commits into
mainfrom
observability-open-band

Conversation

@laulpogan

Copy link
Copy Markdown
Collaborator

Origin

Started from "look at band.ai and compare — should we build an open-source clone of that dashboard?" Research reframed it twice: band.ai (BAND) is not a dashboard, it's a direct wire competitor ($17M seed, closed/centralized SaaS doing wire's exact thesis), and the generic "watch my agents" dashboard is a saturated OSS category. So instead of cloning: ship a wire-aware observability pane, and lean wire into being the open, decentralized BAND.

What's in this PR

P1 — wire dash (src/dash.rs + src/cli/dash.rs): one read-only pane for every wire identity on the box — persona, daemon liveness, pinned peers, relay binding, sync recency. Paired sessions float to the top; idle solo-daemon throwaways collapse to a count (--all expands). --watch (2s), --json (golden wire-dash-v1 surface), --probe (opt-in relay /healthz).

  • Read-only: never spawns/kills a daemon, no per-session network. Verified in-situ against the live 273-session store with pid-count unchanged before/after.
  • Thin by design: enriches the existing session::list_sessions() rather than re-deriving; reuses the cheap pid-liveness check (not the full per-session scan).

P3 — "open BAND" strategy (docs only): docs/design/2026-07-04-open-band-positioning-brief.md (backed by a 30-page primary-source deep-crawl of band.ai, dossier lives in the private dotfiles store) + the architecture doc + anti-features 21–23 (cross-agent memory, task boards, governance-UI theater — each a deliberate divergence verified against BAND's shipped API).

Deferred (honest)

P2 — Mission Control adapter: not in this PR. Its live-status path needs Mission Control's WebSocket schema (not yet nailed); a register-only reporter would be thin shelf-ware. Tracked as a follow-up that starts by fetching MC's openapi.json. A2A-outside interop adapter = separate follow-up spec.

Review + test

Built via the build-loop (MEDIUM tier): 3-reviewer diff gate → 4 MAJORs folded (watch/json one-shot, ANSI column misalignment, self-exclusion on a did-less entry, terminal-escape injection via peer handle) → verifier pass → 1 new MAJOR (EPIPE panic on wire dash | head) folded. Hardening: bounded file reads across the ~270-session fan-out, probe rejects non-http(s) + no redirects.

  • cargo test --lib 628 pass (13 new dash tests incl. golden-shape anti-drift, ESC-strip, self-exclude-by-handle)
  • cargo fmt --check + cargo clippy -D warnings clean
  • In-situ: table renders aligned, --watch --json streams (2 ticks/5s), pipe-to-head exits clean

🤖 Generated with Claude Code

laulpogan and others added 8 commits July 4, 2026 13:30
…ol) + the "open BAND" position

Reframes the band.ai "should we clone the dashboard" question: BAND is a
direct wire competitor ($17M, closed), and the generic agent dashboard is a
saturated OSS category (Mission Control et al.). So: build a wire-aware
observability pane none of them have, reuse an OSS UI where it buys leverage,
and lean wire into being the open/decentralized BAND.

Three sub-projects: P0 shared dash::collect() core, P1 `wire dash` TUI, P2
Mission Control read-only adapter, P3 "open BAND" research-spec brief. Two
product calls locked: cross-agent memory = explicit non-goal; cross-framework
reach = MCP-as-universal-adapter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
…e analysis + roadmap

Backed by a 30-page primary-source deep-crawl of band.ai (dossiers/band-ai.md).
BAND = $17M-seed closed/centralized SaaS doing wire's thesis. Verified wire
wins on identity (self-certifying DID vs account API-key), E2E crypto (BAND has
none), consent (operator-gated vs BAND's LLM-auto-accept option), and
decentralization (BAND coordination plane is SaaS-only). BAND wins on framework
reach (14 native SDKs) + funding.

Recommendation: own "no server owns your agent graph"; close reach
(MCP-as-universal-adapter, sharpened by BAND relegating MCP to automation-only)
+ observability (P1/P2); decline the traps (memory, task boards, governance
theater) as explicit anti-features.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
…om open-BAND brief

Ratified Decline list: cross-agent memory store, task boards/kanban, and
governance-UI/policy-engine are explicit non-goals — each a deliberate
divergence from BAND verified against its shipped API (dossiers/band-ai.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
… box (P0+P1)

New `wire dash` [--watch] [--json] [--all] [--probe]: enumerates every wire
session on the machine (persona, daemon liveness, pinned peers, relay binding,
sync recency), paired sessions first, idle solo daemons collapsed to a count.

P0 src/dash.rs: collect() enriches session::list_sessions() by reading each
session's trust.json (peers, self-excluded), relay.json (binding), daemon.pid,
and last_sync.json mtime — by EXPLICIT path (never the session-scoped
config helpers, which would read the current session 270x). Read-only: never
spawns/kills a daemon, no per-session network; relay /healthz is one deduped
GET, opt-in via --probe. Golden surface: wire-dash-v1 JSON.

P1 src/cli/dash.rs: table (ANSI, no ratatui dep) + --watch 2s redraw + --json.

In-situ vs the live 273-session store: 269 running / 11 paired / 258 idle,
pid-count unchanged before/after (zero spawn/kill). Honest classification:
"idle" = running daemon with no real peers (a live daemon heartbeat-syncs
regardless of use, so peers not sync-age is the signal). P2 (Mission Control
adapter) deferred — needs MC's WebSocket status schema, tracked as a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
…exclude, sanitize

Round-2 fixes from the 3-reviewer diff gate on 2ef5dd4:
- MAJOR: `--watch --json` (and `--watch | pipe` via auto-json) silently
  one-shotted. Watch is now the outer loop; each tick emits JSON or table.
  Verified: 2 JSON ticks in 5s (was 1).
- MAJOR: table misalignment — `{daemon:<9}` padded an ANSI-wrapped string
  (zero fill in color mode) and `fp` column was 7 wide for 8-char fingerprints.
  Pad plain-then-color; fp width 9.
- MAJOR: read_peers self-exclusion failed on a did-less self entry → session
  listed itself as a peer. Now excludes by DID or handle.
- Terminal-escape injection: peer handle/tier + name now run through
  character::sanitize_display_text before render (peer-published text).
- Hardening: bounded (256KB) file reads across the ~270-session fan-out;
  probe_relay rejects non-http(s) + disables redirects; fingerprint_from_did
  returns None on empty tail.
- UX: render cwd (reap-decision hint), pluralize the hidden-count line,
  fleet-over-SSH footer.
- Tests: golden wire-dash-v1 shape anti-drift, StalePid serialization,
  self-exclude-by-handle, ESC-strip, cwd render. 13 dash tests green.
- Doc: design P0 amended to the as-built DaemonState/likely_idle model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
The output path used print!/println!, which PANIC on EPIPE — so `wire dash | head`,
`wire dash --json | jq` closing early, or the Mission Control reporter dropping its
pipe crashed wire with a backtrace (exit 101). Round-2's watch-loop fix made this the
primary failure mode since the tool is now explicitly meant to be piped. Centralize
output through emit(), which exits 0 on BrokenPipe. Verified: `wire dash --all | head`
and `--watch | head` no longer panic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
…ly decommission idle identities

The supervisor keeps a daemon alive for every real identity (so it can receive
mail), so an idle throwaway's daemon can't just be killed — it respawns within a
poll. This adds a retire lifecycle stage: write a `.retired` marker that makes
`supervisor_eligible` treat the home as ineligible (the supervisor's existing
reconcile loop then kills the child and never respawns), plus a direct
graceful-then-force kill for immediacy. Reversible: the marker is the only state
change; `wire revive` removes it and the identity comes back intact (relay slots
never expire, mail is retained, cursor kept).

Surface: `wire retire <handle|fp|key>`, `wire revive <...>`, `wire dash
--retire-idle [--older-than D] [--dry-run] [--force] [--json]`, `wire dash
--retired`. dash gains a `retired` state (collapsed by default, shown by
--retired/--all) + a retired count.

Hardened per a 4-persona plan review (5 blockers folded): retired filtered FIRST
in supervisor_eligible (above the cwd branch AND the max_idle=None early-return);
graceful-then-force kill (bare SIGTERM is a no-op for a headless Windows daemon);
current identity matched by HOME PATH and fail-closed (resolve_session_key is
None on a bare terminal); pending-inbound + paired + recent excluded from the
sweep; --force skips the typed confirm only, never the guards; guards re-checked
per target at kill time (TOCTOU); dry-run default; victims named before confirm.
CLI-only (no MCP tool — an agent must not retire identities unsupervised).

633 lib tests green (supervisor eligibility proven for retired+cwd/identity/
max_idle=None; retire/revive/marker mechanics; box-wide resolver).

NOTE: takes full effect only once the RUNNING supervisor is the new binary —
until `wire service install`/upgrade, the old supervisor respawns retired
daemons. Bulk-retiring the 258 backlog is the operator running --retire-idle
after upgrading.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
…not daemon.pid

daemon.pid mtime resets to now on every supervisor respawn, so on a box whose
supervisor restarted recently it read every daemon as "just started" and the
--older-than guard matched almost nothing (1 of 258). private.key is written
once at keygen and never rewritten → its mtime is the identity's true age, immune
to respawn and to heartbeat sync. Dry-run on the live store now selects 222 at
7d / 257 at 1d, current + paired still correctly excluded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 6, 2026

Copy link
Copy Markdown

Deploying wireup-landing with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9c3b62a
Status: ✅  Deploy successful!
Preview URL: https://6f4b34dc.wireup-landing.pages.dev
Branch Preview URL: https://observability-open-band.wireup-landing.pages.dev

View logs

…home fail-closed

Gate-2 built-thing review (all 5 plan-blockers verified landed) found one new BLOCKER
+ one MAJOR:
- BLOCKER: cmd_retire_idle joined snapshots to homes via a name-keyed HashMap, but
  SessionInfo.name is the DID-derived handle (32-bit space) → ~43% birthday-collision
  at 258 identities → two homes collapse → retire the WRONG identity. Fix: iterate
  list_sessions() directly, each SessionInfo carries its own home_dir. No join.
- MAJOR: current_home() did parent().parent() assuming 2 levels, but a bare terminal's
  config_dir is 1 level (dirs_config/wire) → returned a bogus ancestor instead of None,
  defeating the fail-closed current-identity guard. Fix: verify the resolved home has
  config/wire/private.key, else None.
- MINORs: per-target progress print during the sweep; structured JSON result
  (retired/skipped/errored, not a mixed Vec); consistent is_current in selection;
  added a render test for retired-row collapse.

Re-verified in-situ: 222 candidates @7D / 257 @1d, current excluded, no home collisions.
634 lib tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
@laulpogan

Copy link
Copy Markdown
Collaborator Author

Added: wire retire / wire revive / wire dash --retire-idle (idle-daemon cleanup)

Follow-on to the dash: a way to clean up the 258 idle solo daemons the dash surfaced.

The finding: those aren't husks — each is a real identity (private.key) the supervisor deliberately keeps a daemon for so it can still receive mail. Killing one is futile (respawns in ~10s). So this adds a retire lifecycle stage: write a .retired marker → supervisor_eligible filters retired homes first → the supervisor's own reconcile loop kills the child and never respawns. Reversible (marker-only; wire revive restores; relay slots never expire so mail is retained).

  • wire retire <handle|fp|key> / wire revive <...>
  • wire dash --retire-idle [--older-than D] [--dry-run] [--force] [--json] — dry-run default, typed confirm, guards re-checked at kill time
  • wire dash --retired lists retired identities; dash gains a retired state (collapsed by default)

Guards: never the current identity (by home-path, fail-closed), never paired (mesh), never a pending inbound pair, never younger than the cutoff (identity age via private.key mtime). CLI-only (no MCP — an agent must not retire identities unsupervised).

Built HIGH-tier through the full build-loop: 4-persona plan gate (5 blockers folded) + built-thing gate (1 blocker: a handle-collision in the sweep's home lookup + 1 major: current_home fail-closed — both fixed). Mechanics proven in-situ (retire→dash→revive round-trip; current + paired untouched); dry-run selects 222 idle @7D on the live box. 634 lib tests + full CI green.

⚠️ Takes full effect only once the running supervisor is the new binary — until wire service install/upgrade, the old supervisor respawns retired daemons. Clearing the 258 = merge + restart the wire service, then wire dash --retire-idle.

…bug)

When a Claude session's wire identity displayed (statusline, live session id)
diverges from what the session operates as (a stale MCP/CLI process frozen to an
old session key across a resume), you get "two names" — dial one identity, the
session answers as another. This was invisible until you noticed by hand.

Adds `session::detect_identity_split()`: compares the session key resolved from
the ENV (what a stale process is frozen to) against the PID-file adapter
(Claude Code's authoritative live session, via the parent-process chain). A
mismatch = the split. `wire dash` now prepends a warning banner naming the fix:

  ⚠ identity split — this shell operates as <X> but your live Claude session
    is <Y>. A stale wire process is bound to the wrong identity. Fix: /mcp
    reconnect (or restart this session).

In-process, cross-platform (no ps parsing). Verified in-situ: no banner on a
healthy session, fires on an injected stale WIRE_SESSION_ID. Human view only —
JSON schema unchanged. 636 lib tests green.

Root cause is #351 (wire infers per-session identity with no single stable
source across statusline/MCP/resume); this makes the drift visible + fixable in
one command instead of a mystery. The durable fix (cycle MCP servers on
upgrade/resume) is filed separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmVWMe6fE54U1drShE1yMT
@laulpogan laulpogan merged commit e1ac50a into main Jul 6, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant